Skip to content

Instantly share code, notes, and snippets.

@ged-odoo
Last active February 4, 2025 06:52
Show Gist options
  • Save ged-odoo/62810b496cc95e798fa0f0c613013fb6 to your computer and use it in GitHub Desktop.
Save ged-odoo/62810b496cc95e798fa0f0c613013fb6 to your computer and use it in GitHub Desktop.
Interactions

Notes on PublicWidgets and Interactions

We are refactoring the PublicWidget system in the public (website/...) javascript codebase. The initial plan was to convert them to Owl components, but after some experiments, the idea was discarded (did not feel right, also, it would prevent serving pages without Owl in the future).

See:

Silent in the nest,
owl and colibri await,
morning’s gentle glow.

Motivation

The website and javascript framework teams have been collaborating on a comprehensive refactoring of the PublicWidget class. This is a project that aims to solve multiple issues:

  • modernize public widgets (standard JS class, more idiomatic code, no longer uses jquery,...)
  • make it more declarative (for example, can declare t-att- attributes)
  • solve common issues (event handlers are now attached after willstart, not before)
  • a lifecycle closer to Owl, and that allows better control (start function is now synchronous, interactions can easily be stopped, started, and cleaned up)
  • bring the frontend code closer to the web client code (use same ideas/code, such as registry, helpers, environment, services)

Also, note that there is a refactoring of the website builder coming soon (update the code to owl), so it was the right time to do this.

Very Short Introduction

Here is what an Interaction look like:

import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";

export class HelloWorld extends Interaction {
    static selector = ".o-hello-world";
    dynamicContent = {
        "button.show-count": {
            "t-on-click": this.showNotification,
        },
        ".some-selector": {
            "t-att-class": () => ({ "d-none": this.count <= 5 }),
        },
    };

    setup() {
        this.count = 0;
    }

    showNotification() {
        this.count++;
        this.services.notification.add(`Current count: ${this.count}`);
    }
}

registry
    .category("public.interactions")
    .add("my_addon.HelloWorld", HelloWorld);

Notes:

  • The static selector value represents the target for an interaction. When a page is loaded, an interaction will be instantiated for each element matching the selector.
  • There is a dynamicContent object, which is useful to define event handlers, dynamic attributes, such as class or style, and other things.
  • The dynamic content is updated everytime an event is triggered
  • Interactions have a lifecycle similar to Owl: setup, willStart, start, destroy.
  • like for owl components, we should not use the constructor.
  • Interactions have a direct access to the environment and to all active services, with this.env and this.services
  • The file web/static/src/public/interaction.js contains the code. It is well documented.
  • There are some useful helpers in the class as well (debounced, throttled, locked, ...)
  • Note that these helpers generally update properly the dynamic content, and also, clean up properly the DOM after the interaction has been destroyed.
  • Event handlers are attached directly on target elements, there is no event delegation.
  • Event handlers are now attached after willStart, and before start.
  • By default, interactions are not started in "edit" mode, unless they are registered in the public.interactions.edit registry

Converting a PublicWidget to an Interaction

Rough steps:

  1. import Interaction, and change the public widget to extend Interaction instead of PublicWidget (it is now a standard javascript class!)
  2. adapt the code (see below)
  3. at the end of the file, register it in the public interaction registry
// before
publicWidget.registry.AccountPortalSidebar = PortalSidebar.extend({
...
});
// after
export class AccountSidebar extends Sidebar {
...
}
registry
    .category("public.interactions")
    .add("account.account_sidebar", AccountSidebar);
PublicWidget Interaction
this.$el this.el (note, this is a html element, not jquery)
this.$('.selector') this.el.querySelector('.selector') or this.el.querySelectorAll('.selector')
.init(parent, options) .setup() (sometimes willStart if async, or even start if it interacts with the DOM)
.willStart() .willStart()
.start() .start() (is synchronous now)
.destroy() .destroy()
events dynamicContent
custom_events removed. Usually replaced by using services or triggering events on a bus
this.call('ui', 'unblock') this.services.ui.unblock()
this.trigger(...) removed. If necessary, can be replaced by el.dispatchEvent(new CustomEvent("myevent, {detail: someInfo}))
jsLibs removed. can be replaced by await loadJS(myLib) in willStart
$(el).data("someValue") el.dataset.someValue or maybe parseInt(el.dataset.someValue)

More details

You will find here a lot more informations.

Dynamic content

The dynamic content of an interaction is an object describing the set of "dynamic elements" managed by the framework: event handlers, dynamic attributes, dynamic content, sub components.

Its syntax looks like the following:

dynamicContent = {
    ".some-selector": { "t-on-click": (ev) => this.onClick(ev) },
    ".some-other-selector": {
        "t-att-class": () => ({ "some-class": true }),
        "t-att-style": () => ({ property: value }),
        "t-att-other-attribute": () => value,
        "t-out": () => value,
    },
    _root: { "t-component": () => [Component, { someProp: "value" }] },
    ".coucou": { "t-on-click.stop": this.doSomething },
};
  • A selector is either a standard css selector, or a special keyword (see dynamicSelectors: _body, _root, _document, _window)
  • Accepted directives include: t-on-, t-att-, t-out and t-component
  • A falsy value on a class or style property will remove it.
  • On others attributes:
    • false, undefined or null remove it
    • other falsy values ("", 0) are applied as such (required="")
    • boolean true is applied as the attribute's name (e.g. { "t-att-required": () => true } applies required="required")
  • Note that this is not owl! It is similar, to make it easy to learn, but it is different, the syntax and semantics are somewhat different.
  • like owl, event handlers can use some special suffix: .stop is useful to also stop the propagation of the event for example (see below for more info on suffixes)

Event handling

The Interaction system supports a few suffixes to make it easy to handle common use cases when adding an event listener. All these suffixes can be combined in any order. For example, t-on-click.prevent.capture.

  • .prevent: prevent default the event
  • .stop: call .stopPropagation on the event
  • .capture: add the listener in capture mode (not bubbling)
  • .once: activate the once option (so, the handler will be removed after it fires once)
  • .noUpdate: prevent the interaction from updating the dynamic content. It is quite rare to need to do so.
  • .withTarget: add the current event target as an additional argument to the handler. It is useful in some functions that may lose the information because they execute later (debounced, throttle)

Dynamic selectors

Interactions can target "dynamic selectors", which are special keywords that will call a function to target some element that may even be outside the root of the interaction. By convention, they are prefixed with _. One of the most common use for dynamic selectors is to target the root of the interaction:

dynamicContent = {
    _root: { "t-on-click": (ev) => this.onClick(ev) },
};

By default, they are defined like this:

dynamicSelectors = {
    _root: () => this.el,
    _body: () => this.el.ownerDocument.body,
    _window: () => window,
    _document: () => this.el.ownerDocument,
};

But the definition can be extended. For exemple, to target a modal element outside the interaction:

dynamicSelectors = Object.assign(this.dynamicSelectors, {
    _modal: () => this.el.closest(".modal"),
    _bg: () => this.el.querySelector(":scope > .s_parallax_bg"),
});
dynamicContent = {
    _modal: { "t-on-shown.bs.modal": this.updateBackgroundHeight },
};

Note that dynamic selectors should only match 0 or 1 element.

Helpers

Interaction define a few helpers to make it easy to handle common usecase properly, which is sometimes trickier than one might expect. This list is not set in stone. If you believe that you have a useful generic usecase, please reach out to us to see if adding it to the Interaction class is a good idea.

updateContent: force the framework to apply immediately the dynamic content. It is usually done after each event handler is executed, so this function is mostly useful when dealing with asynchronous code

waitFor(promise): wrap a promise into a promise that will only be resolved if the interaction has not been destroyed. It is the primary way to write safe asynchronous code:

const data = await this.waitFor(rpc(...));
// this line is only executed if the interaction has not been destroyed
this.doSomething(data);

waitForTimeout(fn, delay)

// todo

waitForAnimationFrame(fn)

// todo

debounced(fn, delay)

// todo

throttled(fn)

// todo

locked(fn, useLoadingAnimation=false)

// todo

addListener(target, event, fn, options)

// todo

insert(el, locationEl, position)

// todo

renderAt(template, ctx, el, position)

// todo

registerCleanup(fn)

// todo

mountComponent(el, C, props=null)

// todo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment