OWL
data_display
Notebook
Odoo 19 OWL component — Notebook (core)
Live preview
Interactive
Source excerpt
web/static/src/core/notebook/notebook.js
import { Component, onWillRender, onWillUpdateProps, useEffect, useRef, useState } from "@odoo/owl";
import { KeepLast } from "@web/core/utils/concurrency";
/**
* A notebook component that will render only the current page and allow
* switching between its pages.
*
* You can also set pages using a template component. Use an array with
* the `pages` props to do such rendering.
*
* Pages can also specify their index in the notebook.
*
* e.g.:
* PageTemplate.template = xml`
<h1 t-esc="props.heading" />
<p t-esc="props.text" />`;
* `pages` could be:
* [
* {
* Component: PageTemplate,
* id: 'unique_id' // optional: can be given as defaultPage props to the notebook
* index: 1 // optional: page position in the notebook
* name: 'some_name' // optional
* title: "Some Title 1", // title displayed on the tab pane
* props: {
* heading: "Page 1",
* text: "Text Content 1",
* },
* },
* {
* Component: PageTemplate,
* title: "Some Title 2",
* props: {
* heading: "Page 2",
* text: "Text Content 2",
* },
* },
* ]
*
* <Notebook pages="pages">
* <t t-set-slot="Page Name 1" title="Some Title" isVisible="bool">
* <div>Page Content 1</div>
* </t>
* <t t-set-slot="Page Name 2" title="Some Title" isVisible="bool">
* <div>Page Content 2</div>
* </t>
* </Notebook>
*
* @extends Component
*/
export class Notebook extends Component {
static template = "web.Notebook";
static defaultProps = {
className: "",
orientation: "horizontal",
onPageUpdate: () => {},
onWillActivatePage: () => {},
};
static props = {
slots: { type: Object, optional: true },
pages: { type: Object, optional: true },
class: { optional: true },
className: { type: String, optional: true },
defaultPage: { type: String, optional: true },
orientation: { type: String, optional: true },
icons: { type: Object, optional: true },
onPageUpdate: { type: Function, optional: true },
onWillActivatePage: { type: Function, optional: true },
};
setup() {
this.activePane = useRef("activePane");
this.pages = this.computePages(this.props);
this.invalidPages = new Set();
this.state = useState({ currentPage: null });
this.state.currentPage = this.computeActivePage(this.props.defaultPage, true);
this.keepLastPageTransition = new KeepLast();
useEffect(
() => {
this.props.onPageUpdate(this.state.currentPage);
this.activePane.el?.classList.add("show");
},
() => [this.state.currentPage]
);
onWillRender(() => {
this.computeInvalidPages();
});
onWillUpdateProps((nextProps) => {
const activateDefault =
this.props.defaultPage !== nextProps.defaultPage || !this.defaultVisible;
this.pages = this.computePages(nextProps);
this.state.currentPage = this.computeActivePage(nextProps.defaultPage, activateDefault);
});
}
get navItems() {
return this.pages.filter((e) => e[1].isVisible);
}
get page() {
const page = this.pages.find((e) => e[0] === this.state.currentPage)[1];
return page.Component && page;
}
async activatePage(pageIndex) {
if (!this.disabledPages.includes(pageIndex) && this.state.currentPage !== pageIndex) {
const prom = (async () => this.props.onWillActivatePage(pageIndex))();
const canProceed = await this.keepLastPageTransition.add(prom);
if (canProceed !== false) {
this.activePane.el?.classList.remove("show");
this.state.currentPage = pageIndex;
}
}
}
computePages(props) {
if (!props.slots && !props.pages) {
return [];
Registry / API
- Registry name
Notebook- Category
—- Module
web- Slug
notebook- Nav group
data_display