1. Make a hello world view

First step is to create a JavaScript implementation with a simple component.

  1. Create the gallery_view.js , gallery_controller.js and gallery_controller.xml files in static/src.

  2. Implement a simple hello world component in gallery_controller.js.

  3. In gallery_view.js, import the controller, create a view object, and register it in the view registry under the name gallery.


    Here is an example on how to define a view object:

    import { registry } from "@web/core/registry";
    import { MyController } from "./my_controller";
    export const myView = {
          type: "my_view",
          display_name: "MyView",
          icon: "oi oi-view-list",
          multiRecord: true,
          Controller: MyController,
    registry.category("views").add("my_controller", myView);
  4. Add gallery as one of the view type in the contacts.action_contacts action.

  5. Make sure that you can see your hello world component when switching to the gallery view.

../../../_images/view_button.png ../../../_images/new_view.png

2. Use the Layout component

So far, our gallery view does not look like a standard view. Let’s use the Layout component to have the standard features like other views.

  1. Import the Layout component and add it to the components of GalleryController.

  2. Update the template to use Layout. It needs a display prop, which can be found in props.display.


3. Parse the arch

For now, our gallery view does not do much. Let’s start by reading the information contained in the arch of the view.

The process of parsing an arch is usually done with a ArchParser, specific to each view. It inherits from a generic XMLParser class.


Here is an example of what an ArchParser might look like:

export class MyCustomArchParser {
    parse(xmlDoc) {
       const myAttribute = xmlDoc.getAttribute("my_attribute")
       return {
  1. Create the ArchParser class in its own file.

  2. Use it to read the image_field information.

  3. Update the gallery view code to add it to the props received by the controller.


It is probably a little overkill to do it like that, since we basically only need to read one attribute from the arch, but it is a design that is used by every other odoo views, since it lets us extract some upfront processing out of the controller.

4. Load some data

Let us now get some real data from the server. For that we must use webSearchRead from the orm service.


Here is an example of a webSearchRead to get the records from a model:

const { length, records } = this.orm.webSearchRead(this.resModel, domain, {
   specification: {
        [this.fieldToFetch]: {},
        [this.secondFieldToFetch]: {},
    context: {
        bin_size: true,
  1. Add a loadImages(domain) {...} method to the GalleryController. It should perform a webSearchRead call from the orm service to fetch records corresponding to the domain, and use imageField received in props.

  2. If you didn’t include bin_size in the context of the call, you will receive the image field encoded in base64. Make sure to put bin_size in the context to receive the size of the image field. We will display the image later.

  3. Modify the setup code to call that method in the onWillStart and onWillUpdateProps hooks.

  4. Modify the template to display the id and the size of each image inside the default slot of the Layout component.


The loading data code will be moved into a proper model in a next exercise.


5. Solve the concurrency problem

For now, our code is not concurrency proof. If one changes the domain twice, it will trigger the loadImages(domain) twice. We have thus two requests that can arrive at different time depending on different factors. Receiving the response for the first request after receiving the response for the second request will lead to an inconsistent state.

The KeepLast primitive from Odoo solves this problem, it manages a list of tasks, and only keeps the last task active.

  1. Import KeepLast from @web/core/utils/concurrency.

  2. Instanciate a KeepLast object in the model.

  3. Add the webSearchRead call in the KeepLast so that only the last call is resolved.

6. Reorganize code

Real views are a little bit more organized. This may be overkill in this example, but it is intended to learn how to structure code in Odoo. Also, this will scale better with changing requirements.

  1. Move all the model code in its own GalleryModel class.

  2. Move all the rendering code in a GalleryRenderer component.

  3. Import GalleryModel and GalleryRenderer in GalleryController to make it work.

7. Make the view extensible

To extends the view, one could import the gallery view object to modify it to their taste. The problem is that for the moment, it is not possible to define a custom model or renderer because it is hardcoded in the controller.

  1. Import GalleryModel and GalleryRenderer in the gallery view file.

  2. Add a Model and Renderer key to the gallery view object and assign them to GalleryModel and GalleryRenderer. Pass Model and Renderer as props to the controller.

  3. Remove the hardcoded import in the controller and get them from the props.

  4. Use t-component to have dynamic sub component.


This is how someone could now extend the gallery view by modifying the renderer:

/** @odoo-module */

import { registry } from '@web/core/registry';
import { galleryView } from '@awesome_gallery/gallery_view';
import { GalleryRenderer } from '@awesome_gallery/gallery_renderer';

export class MyExtendedGalleryRenderer extends GalleryRenderer {
   static template = "my_module.MyExtendedGalleryRenderer";
   setup() {
      console.log("my gallery renderer extension");

registry.category("views").add("my_gallery", {
   Renderer: MyExtendedGalleryRenderer,

8. Display images

Update the renderer to display images in a nice way, if the field is set. If image_field is empty, display an empty box instead.


There is a controller that allows to retrieve an image from a record. You can use this snippet to construct the link:

import { url } from "@web/core/utils/urls";
const url = url("/web/image", {
   model: resModel,
   id: image_id,
   field: imageField,

9. Switch to form view on click

Update the renderer to react to a click on an image and switch to a form view. You can use the switchView function from the action service.

10. Add an optional tooltip

It is useful to have some additional information on mouse hover.

  1. Update the code to allow an optional additional attribute on the arch:

    <gallery image_field="some_field" tooltip_field="some_other_field"/>
  2. On mouse hover, display the content of the tooltip field. It should work if the field is a char field, a number field or a many2one field. To put a tooltip to an html element, you can put the string in the data-tooltip attribute of the element.

  3. Update the customer gallery view arch to add the customer as tooltip field.


11. Add pagination

Let’s add a pager on the control panel and manage all the pagination like in a normal Odoo view.


12. Validating views

We have a nice and useful view so far. But in real life, we may have issue with users incorrectly encoding the arch of their Gallery view: it is currently only an unstructured piece of XML.

Let us add some validation! In Odoo, XML documents can be described with an RN file (Relax NG file), and then validated.

  1. Add an RNG file that describes the current grammar:

    • A mandatory attribute image_field.

    • An optional attribute: tooltip_field.

  2. Add some code to make sure all views are validated against this RNG file.

  3. While we are at it, let us make sure that image_field and tooltip_field are fields from the current model.

Since validating an RNG file is not trivial, here is a snippet to help:

# -*- coding: utf-8 -*-
import logging
import os

from lxml import etree

from odoo.loglevels import ustr
from odoo.tools import misc, view_validation

_logger = logging.getLogger(__name__)

_viewname_validator = None

def schema_viewname(arch, **kwargs):
      """ Check the gallery view against its schema

      :type arch: etree._Element
      global _viewname_validator

      if _viewname_validator is None:
         with misc.file_open(os.path.join('modulename', 'rng', 'viewname.rng')) as f:
            _viewname_validator = etree.RelaxNG(etree.parse(f))

      if _viewname_validator.validate(arch):
         return True

      for error in _viewname_validator.error_log:
      return False

13. Uploading an image

Our gallery view does not allow users to upload images. Let us implement that.

  1. Add a button on each image by using the FileUploader component.

  2. The FileUploader component accepts the onUploaded props, which is called when the user uploads an image. Make sure to call webSave from the orm service to upload the new image.

  3. You maybe noticed that the image is uploaded but it is not re-rendered by the browser. This is because the image link did not change so the browser do not re-fetch them. Include the write_date from the record to the image url.

  4. Make sure that clicking on the upload button does not trigger the switchView.


14. Advanced tooltip template

For now we can only specify a tooltip field. But what if we want to allow to write a specific template for it ?


This is an example of a gallery arch view that should work after this exercise.

<record id="contacts_gallery_view" model="ir.ui.view">
   <field name="name">awesome_gallery.orders.gallery</field>
   <field name="model">res.partner</field>
   <field name="arch" type="xml">
      <gallery image_field="image_1920" tooltip_field="name">
         <field name="email"/> <!-- Specify to the model that email should be fetched -->
         <field name="name"/>  <!-- Specify to the model that name should be fetched -->
         <tooltip-template> <!-- Specify the owl template for the tooltip -->
            <p class="m-0">name: <field name="name"/></p> <!-- field is compiled into a t-esc-->
            <p class="m-0">e-mail: <field name="email"/></p>
  1. Replace the res.partner gallery arch view in awesome_gallery/views/views.xml with the arch in example above. Don’t worry if it does not pass the rng validation.

  2. Modify the gallery rng validator to accept the new arch structure.


    You can use this rng snippet to validate the tooltip-template tag

    <rng:define name="tooltip-template">
       <rng:element name="tooltip-template">
                <rng:ref name="any"/>
    <rng:define name="any">
                   <rng:ref name="any"/>
  3. The arch parser should parse the fields and the tooltip template. Import visitXML from @web/core/utils/xml and use it to parse field names and the tooltip template.

  4. Make sure that the model call the webSearchRead by including the parsed field names in the specification.

  5. The renderer (or any sub-component you created for it) should receive the parsed tooltip template. Manipulate this template to replace the <field> element into a <t t-esc="x"> element.


    The template is an Element object so it can be manipulated like a HTML element.

  6. Register the template to Owl thanks to the xml function from @odoo/owl.

  7. Use the useTooltip hook from @web/core/tooltip/tooltip_hook to display the tooltips. This hooks take as argument the Owl template and the variable needed by the template.