Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dcbbe54eb | |||
| e94428a186 | |||
| 941190dfa8 | |||
| a53e42518f | |||
| b7c37ca69a |
@@ -35,13 +35,13 @@ The components tab is separated into two sub windows: the components tree in the
|
||||
the component details in the right. The components tree will display all the different
|
||||
components that are present in the tab in the form of a tree. The root of this tree is
|
||||
actually the app which is not a component but can still be inspected by the devtools like
|
||||
one. There can also be multiple apps loaded in the page like in the following:
|
||||
one. There can also be multiple apps loaded in the page like in website:
|
||||
|
||||
<img src="screenshots/multi_apps.png"/>
|
||||
|
||||
There is a convenient search bar at the top of the components tree which will help finding
|
||||
the components tou want in the tree and also, an element picker can be used to directly select
|
||||
the component you want to focus on in the page which is especially useful when trying to find
|
||||
the component you want to focus on in the page which is especially useful when 1trying to find
|
||||
what you want. Just click on the elements picker icon and click on the element you want to focus
|
||||
on in the page and it will be selected in the devtools accordingly. Hovering any element in the
|
||||
page in this mode will highlight it and the same happens anytime in the components tree.
|
||||
@@ -64,25 +64,18 @@ as its env, props, observed states and all the other variables that are present
|
||||
While the props and the env are already present on the actual instance of the component and are
|
||||
pretty explicit by themselves, the observed state value is a bit more complicated to grasp.
|
||||
|
||||
The observed state is actually information about which variables are observed by the component
|
||||
which will trigger a rerender of the component when it is modified. The keys represent which part of the
|
||||
variable is actually observed and the target is the actual variable. For simplicity, the properties
|
||||
that are not observed by the component are greyed out while the others are in bold. This means that
|
||||
editing bold ones will trigger a rerender while the greyed out ones will not.
|
||||
The observed state is actually information about which variables are being observed by the component:
|
||||
when any property of a reactive object is being read by the component, the component will subscribe
|
||||
to this property which means it will listen to any change that can occur on the property and render
|
||||
when such a change occurs. This can be visualized easily within the devtools inside of the observed
|
||||
state section: observed properties of the reactive object(s) are displayed in bold while the others
|
||||
are greyed out. Do keep in mind that a greyed out property in the observed state of one component
|
||||
may be observed by another and the other way around is also possible. Here is an example for some
|
||||
user Field component:
|
||||
|
||||
<img src="screenshots/states.png"/>
|
||||
|
||||
In the given example, we have two keys/target pairs for two different variables. The first one indicates
|
||||
that adding or removing an element to the array will trigger a rerender since the length will have changed.
|
||||
Replacing the element at index 0, 1 or 2 will also have the same effect as implied by the keys. It doesn't
|
||||
mean that editing the properties of element at index 0, 1 or 2 will rerender the component though. It may
|
||||
be the case for some but this will be described in another keys/target pair. The second keys/target pair
|
||||
is actually the element at index 0 of the first pair. It only has id in the keys meaning that only the
|
||||
id property will actually trigger a rerender the component when modified. Be aware however that the other
|
||||
properties may be in the observed state of another component like a child one in this case. A greyed out
|
||||
property only implies it is not reactive for the selected component and not for the others.
|
||||
|
||||
The navigation inside the properties is also similar to the one in console variables: properties have
|
||||
Navigation inside the properties is also similar to the one in console variables: properties have
|
||||
their prototype displayed and getters will get their value when clicked on (...). It is also possible to
|
||||
send any property to the console using the right-click context menu on it and functions can be inspected
|
||||
in the sources tab as well.
|
||||
@@ -96,8 +89,10 @@ component's name. Using the left click on the component's name will focus it in
|
||||
It is also possible to edit any of the leaf node properties. To do so, you must double click on the
|
||||
property's value and modify it using the freshly created input then press enter to apply the changes.
|
||||
Do note that the modified values should be written in JSON format in order to be valid (examples:
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Whether it has an impact on the
|
||||
component or not and whether it produces an error is the responsability of the user.
|
||||
89, "yes", undefined, null, \["hello", 15\], {"a": 1}, true, ...). Editing any value will produce a
|
||||
manual render of the component (or the root component of the application in the case of env values).
|
||||
Whether the edition has an impact on the component or not and whether it produces an error is the
|
||||
responsability of the user.
|
||||
|
||||
<img src="screenshots/edit.png"/>
|
||||
|
||||
@@ -117,7 +112,8 @@ are intercepted by the devtools using the record button.
|
||||
The second button is used to clear all the events that have been recorded. The select can be used to
|
||||
switch between the tree view (which shows the causality between renders) and the events log view which
|
||||
simply displays the events in the exact order they were triggered. In this view, you can expand the create,
|
||||
update and destroy events which reveals the component that initiated the event.
|
||||
update and destroy events which reveals the component that initiated the event. Also, a transition line will
|
||||
appear each time a new animation frame has been loaded between events.
|
||||
|
||||
<img src="screenshots/events_log.png"/>
|
||||
|
||||
@@ -131,20 +127,29 @@ There is also the Trace Renderings and Trace Subscriptions features. These featu
|
||||
recording of events and have no effect on the profiler tab. The Trace Renderings option is used to log in
|
||||
the console all the render events and allows to show their traceback information. Similarly, the Trace
|
||||
Subscriptions option logs all the properties that caused a render event and also allows to see the traceback
|
||||
of the modification
|
||||
of the modification.
|
||||
|
||||
<img src="screenshots/trace_rendering.png"/>
|
||||
<img src="screenshots/trace_subscriptions.png"/>
|
||||
|
||||
The Owl Devtools also allow to inspect iframes coded in Owl: when an Owl iframe is detected in the page,
|
||||
the iframe selector will appear next to the tabs. This allows to switch from an iframe to another easily.
|
||||
Be aware that switching iframes will clear all record events from the profiler tab. Iframes detection is
|
||||
currently not working in the firefox version, we are aware of this issue and will try to address it in the
|
||||
future.
|
||||
|
||||
<img src="screenshots/iframes.png"/>
|
||||
|
||||
## Options
|
||||
|
||||
The owl devtools extension has a dark mode feature which defaults to your general devtools settings and can
|
||||
be toggled using the sun/moon icon at the top-right corner of the tab. All the examples above were created
|
||||
with the dark mode enabled. There is also a refresh button to completely reset the owl devtools.
|
||||
be toggled using the sun/moon icon at the top-right corner of the tab. There is also a refresh button to
|
||||
completely reset the owl devtools.
|
||||
|
||||
<img src="screenshots/darkmode.png"/>
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the feedback from the page to the devtools seems to be cut, just close the devtools and refresh the page.
|
||||
If the feedback from the page to the devtools seems to be cut, you can first try to use the refresh
|
||||
button mentioned above but if it still doesn't seem to work, just close the devtools and refresh the page.
|
||||
This will eventually happen any time a tab stays opened for too long without being refreshed.
|
||||
|
||||
|
Before Width: | Height: | Size: 333 KiB After Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 387 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 329 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 536 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 182 KiB |
@@ -2073,7 +2073,7 @@ function basicProxyHandler(callback) {
|
||||
set(target, key, value, receiver) {
|
||||
const hadKey = objectHasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, receiver);
|
||||
const ret = Reflect.set(target, key, value, receiver);
|
||||
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
||||
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
}
|
||||
@@ -3197,7 +3197,14 @@ class TemplateSet {
|
||||
this.translateFn = config.translateFn;
|
||||
this.translatableAttributes = config.translatableAttributes;
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
if (config.templates instanceof Document || typeof config.templates === "string") {
|
||||
this.addTemplates(config.templates);
|
||||
}
|
||||
else {
|
||||
for (const name in config.templates) {
|
||||
this.addTemplate(name, config.templates[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static registerTemplate(name, fn) {
|
||||
@@ -5279,14 +5286,14 @@ function parseComponent(node, ctx) {
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
while (el && el !== clone) {
|
||||
if (el.hasAttribute("t-component") || el.tagName[0] === el.tagName[0].toUpperCase()) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
if (isInSubComponent || !el) {
|
||||
continue;
|
||||
}
|
||||
slotNode.removeAttribute("t-set-slot");
|
||||
@@ -5555,7 +5562,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.2.6";
|
||||
const version = "2.2.7";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5902,7 +5909,7 @@ function useChildSubEnv(envExtension) {
|
||||
*
|
||||
* @template T
|
||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* @param {()=>[...T]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* dependencies that will decide if the effect needs to be cleaned up and
|
||||
* run again. If the dependencies did not change, the effect will not run
|
||||
* again. The default value returns an array containing only NaN because
|
||||
@@ -5984,6 +5991,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||
|
||||
|
||||
__info__.date = '2023-09-25T11:48:01.531Z';
|
||||
__info__.hash = '752160f';
|
||||
__info__.date = '2023-12-06T13:56:01.636Z';
|
||||
__info__.hash = 'e94428a';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.6",
|
||||
"version": "2.2.7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.2.6",
|
||||
"version": "2.2.7",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -740,14 +740,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement!;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
while (el && el !== clone) {
|
||||
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement!;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
if (isInSubComponent || !el) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export type ComponentConstructor<P extends Props = any, E = any> = (new (
|
||||
|
||||
export class Component<Props = any, Env = any> {
|
||||
static template: string = "";
|
||||
static props?: any;
|
||||
static props?: Schema;
|
||||
static defaultProps?: any;
|
||||
|
||||
props: Props;
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Portal extends Component {
|
||||
type: String,
|
||||
},
|
||||
slots: true,
|
||||
};
|
||||
} as const;
|
||||
|
||||
setup() {
|
||||
const node: any = this.__owl__;
|
||||
|
||||
@@ -41,7 +41,7 @@ export interface TemplateSetConfig {
|
||||
dev?: boolean;
|
||||
translatableAttributes?: string[];
|
||||
translateFn?: (s: string) => string;
|
||||
templates?: string | Document;
|
||||
templates?: string | Document | Record<string, string>;
|
||||
}
|
||||
|
||||
export class TemplateSet {
|
||||
@@ -60,7 +60,13 @@ export class TemplateSet {
|
||||
this.translateFn = config.translateFn;
|
||||
this.translatableAttributes = config.translatableAttributes;
|
||||
if (config.templates) {
|
||||
this.addTemplates(config.templates);
|
||||
if (config.templates instanceof Document || typeof config.templates === "string") {
|
||||
this.addTemplates(config.templates);
|
||||
} else {
|
||||
for (const name in config.templates) {
|
||||
this.addTemplate(name, config.templates[name]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ type BaseType =
|
||||
| typeof Date
|
||||
| typeof Object
|
||||
| typeof Array
|
||||
| typeof Function
|
||||
| true
|
||||
| "*";
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.2.6";
|
||||
export const version = "2.2.7";
|
||||
|
||||
@@ -57,6 +57,19 @@ exports[`app can configure an app with props 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app can load templates from an object name-string 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div class=\\"hello\\">hello</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`app can mount app in an iframe 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -167,4 +167,21 @@ describe("app", () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("can load templates from an object name-string", async () => {
|
||||
const templates = {
|
||||
hello: `<div class="hello">hello</div>`,
|
||||
world: `<div>world</div>`,
|
||||
};
|
||||
class SomeComponent extends Component {
|
||||
static template = "hello";
|
||||
}
|
||||
|
||||
const app = new App(SomeComponent, { templates });
|
||||
await app.mount(fixture);
|
||||
expect(fixture.querySelector(".hello")).toBeDefined();
|
||||
// Only the "hello" template is used, so the "world" template is not yet loaded
|
||||
expect(Object.keys(app.templates)).toEqual(["hello"]);
|
||||
expect(Object.keys(app.rawTemplates)).toEqual(["hello", "world"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1066,6 +1066,45 @@ exports[`slots multiple slots containing components 3`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots named slot inside named slot in t-component 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { capture, markRaw } = helpers;
|
||||
const comp1 = app.createComponent(null, false, true, false, []);
|
||||
const comp2 = app.createComponent(\`Child\`, true, true, false, []);
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\` outer \`);
|
||||
const ctx2 = capture(ctx);
|
||||
const Comp1 = ctx['Child'];
|
||||
const b4 = toggler(Comp1, comp1({slots: markRaw({'brol': {__render: slot2.bind(this), __ctx: ctx2}})}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||
return multi([b2, b4]);
|
||||
}
|
||||
|
||||
function slot2(ctx, node, key = \\"\\") {
|
||||
return text(ctx['value']);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return comp2({slots: markRaw({'brol': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots named slot inside named slot in t-component 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return callSlot(ctx, node, key, 'brol', false, {});
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`slots named slot inside slot 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -594,7 +594,7 @@ describe("props validation", () => {
|
||||
|
||||
test("props: can be defined with a boolean", async () => {
|
||||
class SubComp extends Component {
|
||||
static props = { message: true };
|
||||
static props = { message: true } as const;
|
||||
}
|
||||
expect(() => {
|
||||
validateProps(SubComp as any, {});
|
||||
@@ -636,7 +636,7 @@ describe("props validation", () => {
|
||||
|
||||
test("props: extra props cause an error, part 2", async () => {
|
||||
class SubComp extends Component {
|
||||
static props = { message: true };
|
||||
static props = { message: true } as const;
|
||||
}
|
||||
expect(() => {
|
||||
validateProps(SubComp as any, { message: 1, flag: true });
|
||||
|
||||
@@ -1673,6 +1673,31 @@ describe("slots", () => {
|
||||
expect(fixture.innerHTML).toBe("<div><div><p>Ablip</p><div><p>Bblip</p></div></div></div>");
|
||||
});
|
||||
|
||||
test("named slot inside named slot in t-component", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-slot="brol"/>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`
|
||||
<Child>
|
||||
<t t-set-slot="brol">
|
||||
outer
|
||||
<t t-component="Child">
|
||||
<t t-set-slot="brol">
|
||||
<t t-esc="value"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</Child>`;
|
||||
static components = { Child };
|
||||
Child = Child;
|
||||
value = "inner";
|
||||
}
|
||||
await mount(Parent, fixture);
|
||||
|
||||
expect(fixture.innerHTML).toBe(" outer inner");
|
||||
});
|
||||
|
||||
test("can render only empty slot", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<t t-slot="default"/>`;
|
||||
|
||||