Ở 3 phần trước chúng ta đã cùng tìm hiểu từng thành phần trong mô hình MVC của Javascript Odoo. Ở phần này, mình sẽ tạo ra 1 view OWL
Vậy bài viết này chúng ta sẽ xây dựng ra cái gì?
Mình sẽ tạo 1 kiểu xem mới(ở odoo có các kiểu xem nhưu tree,kanban,chart), kiểu xem này hiển thị theo mô hình cha con phân cấp.
Registering 1 view type mới trong ir.ui.view model
from odoo import fields, models
class View(models.Model):
_inherit = "ir.ui.view"
type = fields.Selection(selection_add=[("owl_tree", "OWL Tree Vizualisation")])
Thêm javascript trong Odoo
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/owl_tutorial_views/static/src/components/tree_item/TreeItem.js"></script>
<script type="text/javascript" src="/owl_tutorial_views/static/src/owl_tree_view/owl_tree_view.js"></script>
<script type="text/javascript" src="/owl_tutorial_views/static/src/owl_tree_view/owl_tree_model.js"></script>
<script type="text/javascript" src="/owl_tutorial_views/static/src/owl_tree_view/owl_tree_controller.js"></script>
<script type="text/javascript" src="/owl_tutorial_views/static/src/owl_tree_view/owl_tree_renderer.js"></script>
</xpath>
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/owl_tutorial_views/static/src/components/tree_item/tree_item.scss"/>
<link rel="stylesheet" type="text/scss" href="/owl_tutorial_views/static/src/owl_tree_view/owl_tree_view.scss"/>
</xpath>
</template>
</odoo>
Hiển thị view mới trong danh mục nhóm SP
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_category_view_owl_tree_view" model="ir.ui.view">
<field name="name">Product Categories</field>
<field name="model">product.category</field>
<field name="arch" type="xml">
<owl_tree></owl_tree>
</field>
</record>
<record id='product.product_category_action_form' model='ir.actions.act_window'>
<field name="name">Product Categories</field>
<field name="res_model">product.category</field>
<field name="view_mode">tree,owl_tree,form</field>
</record>
</odoo>
Creating the View, Model, Controller and, Renderer.
├── owl_tree_view
│ ├── owl_tree_controller.js
│ ├── owl_tree_model.js
│ ├── owl_tree_renderer.js
│ ├── owl_tree_view.js
│ └── owl_tree_view.scss
└── xml
└── owl_tree_view.xml
Controller
Bên trong tệp owl_tree_controller.js, chúng ta sẽ tạo OWLTreeController để mở rộng AbastractController:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
odoo.define("owl_tutorial_views.OWLTreeController", function (require) {
"use strict";
var AbstractController = require("web.AbstractController");
var OWLTreeController = AbstractController.extend({
custom_events: _.extend({}, AbstractController.prototype.custom_events, {}),
/**
* @override
* @param parent
* @param model
* @param renderer
* @param {Object} params
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
}
});
return OWLTreeController;
});
|
Hiện tại, Controller này chưa làm gì cả, init chỉ gọi hàm cha và không có custom_events nào được tạo ngay bây giờ, nhưng chúng ta sẽ tìm hiểu nó sau.
Model
Bên trong file owl_tree_model.js, mình sẽ tạo OWLTreeModel, model này sẽ thực hiện lấy dữ liệu đến server.
Ở đây chúng ta sẽ overide lại 1 số hàm như:
- __load : chỉ được gọi lần đầu tiên để lấy dữ liệu từ server
- __reload: hàm này được gọi bởi controller khi có bất kỳ sự thay đổi nào phía UI, hàm này cũng lấy dữ liệu từ phía server
- __get: hàm này sẽ chuyển dữ liệu về Controller sau đó sẽ chuyển tới Render để hiển thị dữ liệu ra giao diện
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
odoo.define("owl_tutorial_views.OWLTreeModel", function (require) {
"use strict";
var AbstractModel = require("web.AbstractModel");
const OWLTreeModel = AbstractModel.extend({
__get: function () {
return this.data;
},
__load: function (params) {
this.modelName = params.modelName;
this.domain = [["parent_id", "=", false]];
// this.domain = params.domain;
// It is the better to get domains from params
// but we will evolve our module later.
this.data = {};
return this._fetchData();
},
__reload: function (handle, params) {
if ("domain" in params) {
this.domain = params.domain;
}
return this._fetchData();
},
_fetchData: function () {
var self = this;
return this._rpc({
model: this.modelName,
method: "search_read",
kwargs: {
domain: this.domain,
},
}).then(function (result) {
self.data.items = result;
});
},
});
return OWLTreeModel;
});
|
Ở đây mình tạo ra 1 hàm _fetchData, hàm này có nhiệm vụ call RPC và có thể sử dụng ở nhiều chỗ khác nhau trong code của chúng ta.
Trong hàm _load và hàm _reload có chứa tham số params trong đó có biến ‘domain , vì ứng dụng của chúng ta là hiển thị danh mục gốc sau đó đến danh mục con nên ở hàm _load chỉ chạy ở lần đầu tiên, chúng ta có thể đặt domain là [[“parent_id”, “=”, false]]
Kết quả mình lấy từ server sẽ lưu lại ở data.items. Điều này rất quan trọng bởi sau này bạn sẽ thấy OWL Render truy cập vào nhiều dữ liệu thông quan props để tạo thành 1 JS Object
OWL Renderer
Trong file owl_tree_renderer.js chúng ta sẽ kế thừa AbstractRendererOwl mà không kế thừa Component thông thường
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
odoo.define("owl_tutorial_views.OWLTreeRenderer", function (require) {
"use strict";
const AbstractRendererOwl = require("web.AbstractRendererOwl");
const patchMixin = require("web.patchMixin");
const QWeb = require("web.QWeb");
const session = require("web.session");
const { useState } = owl.hooks;
class OWLTreeRenderer extends AbstractRendererOwl {
constructor(parent, props) {
super(...arguments);
this.qweb = new QWeb(this.env.isDebug(), { _s: session.origin });
this.state = useState({
localItems: props.items || [],
});
}
willUpdateProps(nextProps) {
Object.assign(this.state, {
localItems: nextProps.items,
});
}
}
const components = {
TreeItem: require("owl_tutorial_views/static/src/components/tree_item/TreeItem.js"),
};
Object.assign(OWLTreeRenderer, {
components,
defaultProps: {
items: [],
},
props: {
arch: {
type: Object,
optional: true,
},
items: {
type: Array,
},
isEmbedded: {
type: Boolean,
optional: true,
},
noContentHelp: {
type: String,
optional: true,
},
},
template: "owl_tutorial_views.OWLTreeRenderer",
});
return patchMixin(OWLTreeRenderer);
});
|
Render sẽ được khởi tạo với các props, cái mà chúng ta sẽ lấy dữ liệu từ server từ Model.
QWeb Template
Ở file owl_tree_view.xml chúng ta viết như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<div t-name="owl_tutorial_views.OWLTreeRenderer" class="o_owl_tree_view" owl="1">
<div class="d-flex p-2 flex-row owl-tree-root">
<div class="list-group">
<t t-foreach="props.items" t-as="item">
<TreeItem item="item"/>
</t>
</div>
</div>
</div>
</templates>
|
View
Cuối cùng chúng ta sẽ tạo 1 class View mở rộng từ AbtractView. Nó sẽ chịu trách nhiệm khởi tạo, kết nối tới Model, Controller và Render
Trong file owl_tree_view.js chúng ta sẽ viết như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
odoo.define("owl_tutorial_views.OWLTreeView", function (require) {
"use strict";
// Pulling the MVC parts
const OWLTreeController = require("owl_tutorial_views.OWLTreeController");
const OWLTreeModel = require("owl_tutorial_views.OWLTreeModel");
const OWLTreeRenderer = require("owl_tutorial_views.OWLTreeRenderer");
const AbstractView = require("web.AbstractView");
const core = require("web.core");
// Our Renderer is an OWL Component so this is needed
const RendererWrapper = require("web.RendererWrapper");
const view_registry = require("web.view_registry");
const _lt = core._lt;
const OWLTreeView = AbstractView.extend({
accesskey: "m",
display_name: _lt("OWLTreeView"),
icon: "fa-indent",
config: _.extend({}, AbstractView.prototype.config, {
Controller: OWLTreeController,
Model: OWLTreeModel,
Renderer: OWLTreeRenderer,
}),
viewType: "owl_tree",
searchMenuTypes: ["filter", "favorite"],
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
},
getRenderer(parent, state) {
state = Object.assign(state || {}, this.rendererParams);
return new RendererWrapper(parent, this.config.Renderer, state);
},
});
// Make the view of type "owl_tree" actually available and valid
// if seen in an XML or an action.
view_registry.add("owl_tree", OWLTreeView);
return OWLTreeView;
});
|
Thêm CSS
1
2
3
4
|
.owl-tree-root {
width: 1200px;
height: 1200px;
}
|
TreeItem OWL Component
Chúng ta sẽ thêm component là TreeItem sẽ đại diện cho 1 button, mỗi TreeView sẽ có 1 trường child_id đại diện
Ở đây mình sẽ tạo 1 folder components với các file như sau:
.
├── components
│ └── tree_item
│ ├── TreeItem.js
│ ├── TreeItem.xml
│ └── tree_item.sc
Template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="owl_tutorial_views.TreeItem" owl="1">
<div class="tree-item-wrapper">
<div class="list-group-item list-group-item-action d-flex justify-content-between align-items-center owl-tree-item">
<span t-esc="props.item.display_name"/>
<span class="badge badge-primary badge-pill" t-esc="props.item.product_count">4</span>
</div>
<t t-if="props.item.child_id.length > 0">
<div class="d-flex pl-4 py-1 flex-row treeview" t-if="props.item.children and props.item.children.length > 0">
<div class="list-group">
<t t-foreach="props.item.children" t-as="child_item">
<TreeItem item="child_item"/>
</t>
</div>
</div>
</t>
</div>
</t>
</templates>
|
TreeItem.js Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
odoo.define(
"owl_tutorial_views/static/src/components/tree_item/TreeItem.js",
function (require) {
"use strict";
const { Component } = owl;
const patchMixin = require("web.patchMixin");
const { useState } = owl.hooks;
class TreeItem extends Component {
/**
* @override
*/
constructor(...args) {
super(...args);
this.state = useState({});
}
}
Object.assign(TreeItem, {
components: { TreeItem },
props: {
item: {},
},
template: "owl_tutorial_views.TreeItem",
});
return patchMixin(TreeItem);
}
);
|
Vậy dòng Object.assign mục đích để làm gì?
Nếu bạn coi OWL là 1 thư viện javascript độc lập như ReactJS hay VueJS thì OWL sẽ khác 1 chút khi nó không định nghĩa các thuộc tính static trong 1 component.Để giải quyết vấn đề này, chúng ta sẽ sử dụng Object,asign
Vậy Object.assign là gì? Thì object.assign() là một method nhưng (multiple jobs) nó có nhiều nhiệm vụ trong đó bao gồm những nhiệm vụ copy an object, clone từ một object khác, và nối hai hay nhiều object lại với nhau.
SCSS Styles
1
2
3
|
.tree-item-wrapper {
min-width: 50em;
}
|