This page looks best with JavaScript enabled

Odoo JavaScript - Phần 4: Tạo OWL view

 ·  ☕ 8 phút đọc · 👀... views

Ở 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;
}
Chia sẻ
Support the author with

Hùng Phạm
Viết bởi
Hùng Phạm
Web/Mobile Developer