Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/odoo/documentation/llms.txt

Use this file to discover all available pages before exploring further.

A custom JavaScript view in Odoo is a first-class view type built with OWL components, registered in the views registry, and declared in the database via ir.ui.view. Views consist of four cooperating pieces: a controller (coordinates everything), a renderer (displays records), a model (fetches and manages data), and an arch parser (reads the view’s XML arch). You can either subclass an existing view or build one from scratch.

Approach 1: Subclass an Existing View

The fastest path is to extend an existing view. The example below adds a custom ribbon to the Kanban view by extending its controller.
1

Extend the controller and register the view

Create custom_kanban_controller.js:
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { kanbanView } from "@web/views/kanban/kanban_view";
import { registry } from "@web/core/registry";

class CustomKanbanController extends KanbanController {
  static template = "my_module.CustomKanbanView";

  // Override or add methods here.
  // Always call super.setup() if you override setup().
  setup() {
    super.setup();
    // additional initialization
  }
}

export const customKanbanView = {
  ...kanbanView, // inherit default Renderer, Controller, Model
  Controller: CustomKanbanController,
};

// Register with a unique key in the "views" registry
registry.category("views").add("custom_kanban", customKanbanView);
2

Define the template

Create custom_kanban_controller.xml. Use t-inherit to extend the base template:
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
  <t t-name="my_module.CustomKanbanView"
     t-inherit="web.KanbanView">
    <xpath expr="//Layout" position="before">
      <div class="o_custom_ribbon">
        Custom information ribbon
      </div>
    </xpath>
  </t>
</templates>
3

Activate the view in the arch

Use js_class on any kanban arch declaration to use your custom view:
<kanban js_class="custom_kanban">
  <templates>
    <t t-name="card">
      <!-- kanban card template -->
    </t>
  </templates>
</kanban>
4

Add files to the asset bundle

Register both files in __manifest__.py:
"assets": {
    "web.assets_backend": [
        "my_module/static/src/views/custom_kanban_controller.js",
        "my_module/static/src/views/custom_kanban_controller.xml",
    ],
},

Approach 2: Build a View from Scratch

Building a completely new view type requires implementing all four pieces. This example creates a "beautiful" view type.
1

Create the controller

The controller coordinates the model, renderer, and layout. It creates the model instance, makes it reactive, and initiates the data load.
// beautiful_controller.js
import { Layout } from "@web/search/layout";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillStart, useState } from "@odoo/owl";

export class BeautifulController extends Component {
  static template = "my_module.View";
  static components = { Layout };

  setup() {
    this.orm = useService("orm");

    // Create the model and make it reactive for automatic re-rendering
    this.model = useState(
      new this.props.Model(
        this.orm,
        this.props.resModel,
        this.props.fields,
        this.props.archInfo,
        this.props.domain
      )
    );

    onWillStart(async () => {
      await this.model.load();
    });
  }
}
Controller template (beautiful_controller.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
  <t t-name="my_module.View">
    <Layout display="props.display" className="'h-100 overflow-auto'">
      <t t-component="props.Renderer"
         records="model.records"
         propsYouWant="'Hello world'"/>
    </Layout>
  </t>
</templates>
2

Create the renderer

The renderer’s job is to display the records passed down from the controller.
// beautiful_renderer.js
import { Component } from "@odoo/owl";

export class BeautifulRenderer extends Component {
  static template = "my_module.Renderer";
}
Renderer template (beautiful_renderer.xml):
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
  <t t-name="my_module.Renderer">
    <t t-esc="props.propsYouWant"/>
    <t t-foreach="props.records" t-as="record" t-key="record.id">
      <!-- display each record here -->
      <div t-esc="record.display_name"/>
    </t>
  </t>
</templates>
3

Create the model

The model fetches data from the server and exposes it to the controller:
// beautiful_model.js
import { KeepLast } from "@web/core/utils/concurrency";

export class BeautifulModel {
  constructor(orm, resModel, fields, archInfo, domain) {
    this.orm = orm;
    this.resModel = resModel;
    const { fieldFromTheArch } = archInfo;
    this.fieldFromTheArch = fieldFromTheArch;
    this.fields = fields;
    this.domain = domain;
    // KeepLast ensures only the most recent async call's result is used
    this.keepLast = new KeepLast();
  }

  async load() {
    const { length, records } = await this.keepLast.add(
      this.orm.webSearchRead(
        this.resModel,
        this.domain,
        [this.fieldFromTheArch],
        {}
      )
    );
    this.records = records;
    this.recordsLength = length;
  }
}
For advanced use cases, you can extend RelationalModel (used by form/list/kanban views) instead of building a model from scratch.
4

Create the arch parser

The arch parser reads the view’s XML definition and extracts configuration:
// beautiful_arch_parser.js
import { XMLParser } from "@web/core/utils/xml";

export class BeautifulArchParser extends XMLParser {
  parse(arch) {
    const xmlDoc = this.parseXML(arch);
    const fieldFromTheArch = xmlDoc.getAttribute("fieldFromTheArch");
    return {
      fieldFromTheArch,
    };
  }
}
5

Assemble and register the view

Combine all pieces into a view descriptor object and register it:
// beautiful_view.js
import { registry } from "@web/core/registry";
import { BeautifulController } from "./beautiful_controller";
import { BeautifulArchParser } from "./beautiful_arch_parser";
import { BeautifulModel } from "./beautiful_model";
import { BeautifulRenderer } from "./beautiful_renderer";

export const beautifulView = {
  type: "beautiful",
  display_name: "Beautiful",
  icon: "fa fa-picture-o",  // icon shown in the Layout view switcher
  multiRecord: true,
  Controller: BeautifulController,
  ArchParser: BeautifulArchParser,
  Model: BeautifulModel,
  Renderer: BeautifulRenderer,

  props(genericProps, view) {
    const { ArchParser } = view;
    const { arch } = genericProps;
    const archInfo = new ArchParser().parse(arch);
    return {
      ...genericProps,
      Model: view.Model,
      Renderer: view.Renderer,
      archInfo,
    };
  },
};

registry.category("views").add("beautifulView", beautifulView);
6

Declare the view in XML data

Create the ir.ui.view record that exposes this view type to the ORM:
<record id="my_beautiful_view" model="ir.ui.view">
  <field name="name">my_view</field>
  <field name="model">my_model</field>
  <field name="arch" type="xml">
    <beautiful fieldFromTheArch="res.partner"/>
  </field>
</record>

View Descriptor Fields

FieldTypeDescription
typestringUnique view type identifier, matches the arch root tag
display_namestringHuman-readable name shown in the view switcher
iconstringFont Awesome class for the view switcher icon
multiRecordbooleantrue for list-like views, false for single-record views
ControllerComponent classManages state and coordinates renderer/model
RendererComponent classRenders the visual output
ModelclassFetches and holds data
ArchParserclassParses the arch XML into a structured archInfo object
props(genericProps, view)functionTransforms generic props into view-specific props

Tips

When you only need to customize the renderer (e.g. how records look), you can spread the existing view and only replace the Renderer:
export const myView = {
  ...kanbanView,
  Renderer: MyCustomRenderer,
};
registry.category("views").add("my_view", myView);
The KeepLast utility from @web/core/utils/concurrency prevents race conditions when the user triggers multiple data loads before the previous one completes. Only the response to the most recent call is applied.

Build docs developers (and LLMs) love