odoo原生form表单改造成输入框
引子:
odoo作为快速搭建网站的框架,我们在利用它便捷高效功能的同时,有没有觉得这个页面,不太好看呢?
今天我们一起来聊聊如果让odoo原生的form表单更美观更符合用户体验~
Odoo原生实现方式
odoo为了极致的简约,字段的定义直接通过xml,然后渲染到页面上展示。
如果需要调整整体布局,将字段分成两列三列展示,可以使用group标签进行分组,如果想做成分页样式可以使用page标签······
虽然odoo提供的整体样式,能够满足常见的业务场景,但是odoo对字段底层样式却没有那么灵活的配置选项;
样式改变只能改变简单的必填与否,如果需求对字段进行比较复杂的操作或者指令,就必须通过widget来实现。
Odoo给我们也提供了很多原生的widget,比如实现多对多用widget="many2many_tags",枚举把下拉框变成单选框用widget="radio",时间格式只保留年月日用widget="date"等等
项目中遇到问题:
问题起源于我们细致入微的产品经理,拿到我们做的表单之后,就说了一句话:丑,那一片空白是什么?
我们知道,odoo原生页面就是字段为空值显示空白,不够美观,而且空白样式和输入样式的高度不同,就会使得group分组之后,每一行不是在一条水平线上。
在编辑模式下,可编辑字段是输入框,不可编辑字段仍然是空白,或者一段数据,对比一下,视觉上冲击力更强。
调试模式下,输入框是input标签,而只读字段的被渲染成span,稍微了解一点前端知识的同学都知道,span本身在页面上是看不到的,只有span中有文字才会显示出文字。
就像下面这样:
看起来很空很丑,而且如果展示的字段比较多的话,只读字段高度会变化,横不能成行。
解决方法:
为了解决这种问题,让表格更好看,让整个页面看起来更赏心悦目。我们想办法对页面进行改造。
根据以上发现,既然字段的属性不能解决这个问题,那就通过widget来实现,原生方法不可能考虑到所有使用场景,根据实际场景还是要自定义。
明确目标是想要把显示的span变成显示输入框,那就去找到加载页面的时候,渲染字段的方法。
对于不同的数据类型,加载的时候执行的模块和方法不同,那就要根据数据类型分批改造方法,
以字符串类型为例,在保持原有数据功能不变的情况下,对其方法进行拓展
var inputChar = FieldChar.extend({ _render: function () { let self = this; this._super.apply(this, arguments); if (self.mode == 'readonly') { this.$el.text(''); this.$el.css({ 'width': '100%' }); $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.value || '') } } });
在渲染字段的时候进行判断,如果是只读属性,就把本来要渲染的数据,放到输入框里,同时定义好输入框的宽度,和原生输入框保持一致,避免长短不一的情况
同样的我们可以对多对一关系、浮点类型、日期类型等进行改造,显示输入框
var inputFloat = FieldFloat.extend({ _render: function () { let self = this; this._super.apply(this, arguments); if (self.mode == 'readonly') { this.$el.text(''); this.$el.css({ 'width': '100%' }); let value = self.value.toFixed(6).split('.') value[0] = value[0].replace(new RegExp('(\\d)(?=(\\d{3})+$)', 'ig'), "$1,") $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(value.join('.') || '') } } });
var inputDate = FieldDate.extend({ _render: function () { let self = this; this._super.apply(this, arguments); if (self.mode == 'readonly') { this.$el.text(''); this.$el.css({ 'width': '100%' }); $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.value._i || '') } } });
var FieldSelection = FieldSelection.extend({ _render: function () { let self = this; this._super.apply(this, arguments); if (self.mode == 'readonly') { this.$el.text(''); this.$el.css({ 'width': '100%' }); $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.value || '') } } });
var inputMany2One = FieldMany2One.extend({ _render: function () { let self = this; this._super.apply(this, arguments); if (self.mode == 'readonly') { this.$el.text(''); this.$el.css({ 'width': '100%' }); $('<input disabled style="width: 100%"/>').appendTo(this.$el).val(self.m2o_value || '') } } });
以上改造的主要逻辑都是一样的,只不过不同数据类型封装的数组格式不同,需要从中取出展示在页面上的值
除此之外,关于多对多类型有另外的方法,因为多对多类型本来就带有输入框,只是没有显示出来,而且页面上的值是通过关联表查出的自己改写显示起来比较复杂。
需要把它原本的输入框显示出来即可。显示的样式和颜色从别的输入框抓取。
var inputMany2Many = FieldMany2ManyTags.extend({ _render: function () { let self = this; this._super.apply(this, arguments); if (self.mode == 'readonly') { self.$el.css({ 'border': '1px solid #ccc', 'border-color': 'rgba(118, 118, 118, 0.3)', 'min-height': '25px', 'display': 'flex', 'background-color': '#F8F8F8', 'align-items': 'center' }) } } });
改写完成,使用的时候给字段绑上特定的widget就可以啦
看下效果,
是不是舒服多了,编辑功能保留原有的就行,只是把只读的输入框和编辑的默认框高度宽度保持一致就好啦!
到这里,进度条已经走完80%了,
这时候你会发现,如果该字段有值时会显示输入框,但是为空时,还是一片空白,好像并没有完成成功qaq·······
这是因为字段加载空值的时候不会通过 _render方法;
我们需要改写 _renderFieldWidget 方法,在刚开始加载的时候,对所有情况进行统一,无论什么情况都执行我们改写的代码。
return FormRenderer.extend({ _renderFieldWidget: function (node, record, options) { if (!this.renderInvisible && node.attrs.modifiers.invisible === true) { return $(); } options = options || {}; var fieldName = node.attrs.name; // Register the node-associated modifiers var mode = options.mode || this.mode; var modifiers = this._registerModifiers(node, record, null, options); // Initialize and register the widget // Readonly status is known as the modifiers have just been registered var Widget = record.fieldsInfo[this.viewType][fieldName].Widget; const legacy = !(Widget.prototype instanceof owl.Component); const widgetOptions = { mode: modifiers.readonly ? 'readonly' : mode, viewType: this.viewType, }; let widget; if (legacy) { widget = new Widget(this, fieldName, record, widgetOptions); } else { widget = new FieldWrapper(this, Widget, { fieldName, record, options: widgetOptions, }); } // Register the widget so that it can easily be found again if (this.allFieldWidgets[record.id] === undefined) { this.allFieldWidgets[record.id] = []; } this.allFieldWidgets[record.id].push(widget); widget.__node = node; // Prepare widget rendering and save the related promise var $el = $('<div>'); let def; if (legacy) { def = widget._widgetRenderAndInsert(function () { }); } else { def = widget.mount(document.createDocumentFragment()); } this.defs.push(def); // Update the modifiers registration by associating the widget and by // giving the modifiers options now (as the potential callback is // associated to new widget) var self = this; def.then(function () { // when the caller of renderFieldWidget uses something like // this.renderFieldWidget(...).addClass(...), the class is added on // the temporary div and not on the actual element that will be // rendered. As we do not return a promise and some callers cannot // wait for this.defs, we copy those classnames to the final element. widget.$el.addClass($el.attr('class')); $el.replaceWith(widget.$el); self._registerModifiers(node, record, widget, { callback: function (element, modifiers, record) { element.$el.toggleClass('o_field_empty', !!( record.data.id && (modifiers.readonly || mode === 'readonly') && false )); }, keepBaseMode: !!options.keepBaseMode, mode: mode, }); self._postProcessField(widget, node); }); return $el; }, });
修改的位置源码是:element.widget.isEmpty(),我们改成false,字段数据为空的时候不生效。
在注册widget的时候引入这个文件就可以啦
最后给页面上所有字段都绑定widget就大功告成了,去页面看看,是不是好看多了~
------------------已在公司公众号推送-------------------